package com.webserver.http;


import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import static com.webserver.http.HttpContext.CR;
import static com.webserver.http.HttpContext.LF;

/**
 * @author 老安
 * @data 2022/7/7 21:05
 * 请求对象
 * 该类的每一个实例用于表示HTTP协议规定的客户端发送过来的一个请求内容
 * 每个请求由三部分组成
 * 请求行,消息头,消息正文
 */
public class HttpServletRequest {
    //请求行的相关信息
    private String method;//请求方式
    private String uri;//抽象路径
    private String protocol;//协议版本
    private String requestURI;//存uri中"?"左侧的请求部分
    private String queryString;//存uri中"?"右侧的参数部分
    //存客户端提交上来的每一组参数
    private Map<String, String> parameter = new HashMap<>();
    //存储消息头的相关信息
    private Map<String, String> headers = new HashMap<>();
    private Socket socket;

    /*
     * 实例化请求对象的过程也是解析的过程
     * */
    public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
        this.socket = socket;
        //1.1解析请求行
        parseRequestLine();
        //1.2解析消息头
        parseHeaders();
        //1.3解析消息正文
        parseContent();
    }

    //解析请求行
    private void parseRequestLine() throws IOException, EmptyRequestException {
        String line = readLine();
        //如果line内容是null,说明本次的请求是空请求,就对外抛出空请求异常,后面的内容都不会执行
        if (line.isEmpty()) {
            throw new EmptyRequestException();
        }
        //GET /myweb/index.html HTTP/1.1
        System.out.println("请求行:" + line);
        //请求行的三部分是由空格分隔的,所以可以利用正则表达式,根据空格拆分
        //[GET,/myweb/index.html,HTTP/1.1]
        String[] data = line.split("\\s");
        method = data[0];
        uri = data[1];
        protocol = data[2];
        //进一步解析uri
        parseUri();
        //启动服务器,测试localhost:8088/myweb/index.html
        System.out.println("请求方式:" + method);//GET
        System.out.println("抽象路径:" + uri);// /myweb/index.html
        System.out.println("协议版本:" + protocol);//HTTP/1.1
    }

    /*
     * 进一步解析uri
     * */
    private void parseUri() {
        /*
         * uri是有两种情况 1是不含有参数的 2是含有参数
         * 不含参数:/myweb/index.html
         * 含有参数:/myweb/reg?username=anguojian&password=12222&nickname=1212&age=23
         * 处理方式:
         * 1:如果不含有参数,则直接将uri的值赋值给requestURI即可
         * 2如过有参数,就需要解析
         *   2.1:先将uri部分按照"?"
         *       将请求部分赋值给requestURI
         *       将参数部分赋值给queryString
         *   2.2:再将queryString按照"&"拆分出每一组参数
         *       每组参数再按照"="拆分为参数名和参数值
         *       并将参数名作为key,参数值作为value存储到parameter这个Map中
         *       允许页面中输入框是空着的,这种情况参数值为null,存到parameter即可
         * */
        //先将uri部分按照"?"进行拆分
        //如果含参[/myweb/reg,username=anguojian&password=12222&nickname=1212&age=23]
        //如果不含参[/myweb/index.html]
        String[] data = uri.split("\\?");
        //将请求部分赋值给requestURI
        requestURI = data[0];
        //判断如果data数组的长度大于1,则说明拆分的结果中含有参数
        if (data.length > 1) {
            //将参数部分赋值给queryString
            //username=anguojian&password=12222&nickname=1212&age=23
            queryString = data[1];
            //按照"&"符号进行拆分
            //[username=anguojian,password=12222,nickname=1212,age=23]
            parseParameters(queryString);
        }
        System.out.println("请求部分:" + requestURI);
        System.out.println("参数部分:" + queryString);
        System.out.println("参数Map:" + parameter);
    }

    /*
     * 解析参数
     */
    private void parseParameters(String line) {
        //先转码
        try {
            line = URLDecoder.decode(line, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String[] data = line.split("&");
        for (String para : data) {
            //将每一组元素根据"="拆分
            String[] paras = para.split("=");
            //可能输入框输入内容为空 username= 根据=拆分,数组中只有参数名
            parameter.put(paras[0], paras.length > 1 ? paras[1] : null);
        }
    }

    //解析消息头
    private void parseHeaders() throws IOException {
        //由于不同的情况下,消息头的个数不一样,所以我们就一直读取消息头
        while (true) {
            String line = readLine();
            //由于消息头最后一行是CR+LF的空白行,而输出时,是CR输出,经由trim方法,会变成空字符串,所以当我们调用readLine时,读到的是空字符串时,就可以断定消息头读取结束了
            if (line.isEmpty()) {
                //if (line.length()==0) {
                //if ("".equals(line)) {
                break;//如果是空串,就停止读一行的操作,消息头读取结束
            }
            System.out.println("消息头:" + line);
            //将接收到的消息头,根据冒号空格拆分!!!
            //此时,data中第一个元素是消息头的名字,第二个元素是消息头的值
            String[] data = line.split(":\\s");
            headers.put(data[0], data[1]);
        }
    }

    //解析消息正文
    private void parseContent() throws IOException {
        System.out.println("开始解析消息正文...");
        //通过判断请求中消息头是否包含Content-Length来判断是否有正文
        if (headers.containsKey("Content-Length")) {
            int contentLength = Integer.parseInt(headers.get("Content-Length"));
            System.out.println("请求的消息正文的长度:" + contentLength);
            //基于消息头告知的正文长度来创建一个字节数组,用于保存正文的内容
            byte[] contentData = new byte[contentLength];
            //读取正文中所有的字节
            InputStream in = socket.getInputStream();
            //将读取到的内容都存入到contentData数组中
            in.read(contentData);
            //获取消息头Content-Type
            String contentType = headers.get("Content-Type");
            //根据Content-Type的值如果是application/x-www-form-urlencoded
            //说明消息正文一定是字符串,并且还是key=value的格式
            if ("application/x-www-form-urlencoded".equals(contentType)) {
                //将数组通过字符串的构造方法转换为字符串,并且指定编码
                String line = new String(contentData, StandardCharsets.ISO_8859_1);
                System.out.println("请求的消息正文的内容:" + line);
                parseParameters(line);
            }
        }
    }

    /*
     * 读取一行字符串,调用这个方法就可以返回一行字符串内容
     * */
    private String readLine() throws IOException {
        /*
         * 按照HTTP协议的规定:浏览器除了附件内容外,其余都是字符串,
         * 但是只包含英文,数字,符号,这三种有一个特点:就是全是单字节.
         * 所以每个字符就是一个字节
         */
        //获取链接客户端的输入流(客户端输入的内容可以通过这个流获取)
        InputStream in = socket.getInputStream();
        //用于高效拼接字符串
        StringBuilder builder = new StringBuilder();
        int d;
        //pre 上一次读取的字符 cur 本次读取的字符
        char pre = 'a', cur = 'a';
        while ((d = in.read()) != -1) {//read() 读取一个字节,读不到返回-1
            //将本次读取的字节,转换为字符赋值给cur
            cur = (char) d;
            //判断如果上一次读取的字符是回车(CR=13)并且本次读取到的是换行(LF=10)
            //如果成立,说明连续读到了回车+换行,一行字符串读取结束
            if (pre == CR & cur == LF) {
                break;
            }
            //将本次读取到的字符拼接到StringBuilder中
            builder.append(cur);
            //将本次读取的字符,赋值给上一次读取的字符
            pre = cur;
        }
        //line中包含了回车符,回车符在输出时,是一个空白符,所以使用trim删除字符串中的空白符
        return builder.toString().trim();
    }

    //生成对应属性的get方法,不需要set方法,因为请求信息是浏览器发送,我们无权修改
    public String getMethod() {
        return method;
    }

    public String getUri() {
        return uri;
    }

    public String getProtocol() {
        return protocol;
    }

    /*
     * 根据给定的消息头的名字获取对应的值
     * 返回值 是value
     * 参数 是key
     * */
    public String getHeaders(String name) {
        return headers.get(name);
    }

    public String getRequestURI() {
        return requestURI;
    }

    public String getQueryString() {
        return queryString;
    }

    /*
     * 根据给定的参数名获取对应的参数值
     */
    public String getParameter(String name) {
        return parameter.get(name);
    }
}
